package services;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.uml2.uml.Association;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.DataType;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.Property;
import org.eclipse.uml2.uml.Relationship;
import org.eclipse.uml2.uml.Type;

import fr.obeo.acceleo.gen.template.eval.ENodeCastException;

public class Uml2Services {

	private StringServices stringService = new StringServices();

	/**
	 * Contiene la información de todas las asociaciones del modelo.
	 */
	private List<AssociationInfo> associations = new ArrayList<AssociationInfo>();

	/**
	 * Estructura la información de una asociación.
	 * 
	 * @author emilio
	 */
	@SuppressWarnings("unused")
	private class AssociationInfo {
		// TODO: utilizar el nombre de la asociación para algo.
		private String name;
		private Side side1;
		private Side side2;

		public AssociationInfo(Association as) {
			name = as.getName();
			side1 = new Side(as, 0);
			side2 = new Side(as, 1);
		}

		public Side getOppositeSide(org.eclipse.uml2.uml.Class fromClass) {
			if (side1.tt.equals(fromClass)) {
				return side2;
			} else {
				return side1;
			}
		}

		/**
		 * Estructura la información de uno de los lados de la asociación.
		 * 
		 * @author emilio
		 */
		private class Side {

			public Side(Association as, int side) {
				Type t = as.getEndTypes().get(side);
				tt = t;
				if (t instanceof Class) {
					type = ((Class) t).getName();
				} else if (t instanceof DataType) {
					type = ((DataType) t).getName();
				}
				Property p = as.getAllAttributes().get(side);
				role = p.getName();
				lower = p.getLower();
				upper = p.getUpper();
				if (upper < 0) {
					many = true;
				}
				navigable = p.isNavigable();
				composite = p.isComposite();
				_static = p.isStatic();
				ordered = p.isOrdered();
				unique = p.isUnique();
				readonly = p.isReadOnly();
			}

			/**
			 * El tipo de elemento del lado.
			 */
			private Type tt;
			/**
			 * Indica si el lado es "muchos"
			 */
			private boolean many;
			/**
			 * El nombre del tipo de elemento de uno de los lados (Clase,
			 * DataType)
			 */
			private String type;
			/**
			 * El nombre del rol de uno de los lados
			 */
			private String role;
			/**
			 * La cardinalidad mínima del lado
			 */
			// TODO: utilizar el valor lower de una relación para algo
			private int lower;
			/**
			 * La cardinalidad máximima del lado (-1 significa '*')
			 */
			private int upper;
			/**
			 * Indica si de ese lado hay navegabilidad.
			 */
			// TODO: utilizar el estado de 'navigable' de las relaciones para
			// algo.
			private boolean navigable;
			/**
			 * Indica si el lado es estático.
			 */
			// TODO: utilizar el estado de 'static' de las relaciones para algo.
			private boolean _static;
			/**
			 * Indica si el lado es único.
			 */
			// TODO: utilizar el estado de 'unique' de las relaciones para algo.
			private boolean unique;
			/**
			 * Indica si el lado es composición.
			 */
			// TODO: utilizar el estado de 'composite' de las relaciones para
			// algo.
			private boolean composite;
			/**
			 * Indica si el lado es ordenado.
			 */
			// TODO: utilizar el estado de 'ordered' de las relaciones para
			// algo.
			private boolean ordered;
			/**
			 * Indica si el lado es de solo lectura
			 */
			// TODO: utilizar el estado de 'readonly' de las relaciones para
			// algo.
			private boolean readonly;
		}
	}

	/**
	 * Retorna los getters y setters correspondientes a la clase.
	 * 
	 * @param ele
	 * @return
	 * @throws ENodeCastException
	 */
	public String generateAssociationAccessors(Element ele)
			throws ENodeCastException {

		relationships(ele);

		StringBuilder sb = new StringBuilder();
		for (AssociationInfo ai : associations) {
			AssociationInfo.Side oppositeSide = ai.getOppositeSide((Class) ele);
			String annotation;
			if (oppositeSide.many) {
				annotation = TemplateReader.read(this,
						"JPA.OneToManyAssociation.template.txt",
						oppositeSide.role, oppositeSide.type,
						stringService.firstUpper(oppositeSide.role));
			} else {
				annotation = TemplateReader.read(this,
						"JPA.ManyToOneAssociation.template.txt",
						oppositeSide.role, oppositeSide.type,
						stringService.firstUpper(oppositeSide.role));
			}
			sb.append(annotation);
		}
		return sb.toString();
	}

	/**
	 * Dada una clase, retorna todas las asociaciones en las que juega algún
	 * rol.
	 * 
	 * @return
	 * @throws ENodeCastException
	 */
	private void relationships(Element ele) throws ENodeCastException {
		associations.clear();
		for (Relationship r : ele.getRelationships()) {
			if (r instanceof Association) {
				this.associations.add(new AssociationInfo((Association) r));
			}
		}
	}

	/**
	 * Indica si el elemento es una clase con estereotipo "Entity"
	 * @param elt
	 * @return
	 */
	public boolean isEntity(org.eclipse.uml2.uml.Element elt) {
		return hasStereotype(elt, "Entity");
	}

	/**
	 * Verify if an Element have a stereotype. Use keyword and profil to find
	 * stereotype. Multiple stereotype are allow.
	 * 
	 * @param elt
	 *            Element used.
	 * @param stereotype
	 *            Stereotype to search.
	 * @return true if found. false else.
	 * @throws ENodeCastException
	 */
	public boolean hasStereotype(org.eclipse.uml2.uml.Element elt, String stereotype) {
		if (elt instanceof org.eclipse.uml2.uml.Class) {
			// search with real stereotype
			org.eclipse.uml2.uml.Stereotype stereotypeFound = elt.getApplicableStereotype(stereotype);
			
			for (org.eclipse.uml2.uml.Stereotype s : elt.getApplicableStereotypes()) {
				if (s.getName().equals(stereotype)) {
					stereotypeFound = s;
				}
			}
			if (stereotypeFound == null) {
				// search with keywords
				if (elt.hasKeyword(stereotype)) {
					return true;
				}
			} else {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Indica si la clase extiende a alguna otra
	 * @param elt
	 * @return
	 */
	public boolean hasSuperclass(org.eclipse.uml2.uml.Element elt) {
		return getSuperclass(elt).length() > 0;
	}

	/**
	 * Retorna el nombre de la superclase o un string vacío si no tiene.
	 * @param elt
	 * @return
	 */
	public String getSuperclass(org.eclipse.uml2.uml.Element elt) {
		if (elt instanceof org.eclipse.uml2.uml.Class) {
			org.eclipse.uml2.uml.Class cls = (org.eclipse.uml2.uml.Class)elt;
			if (cls.getSuperClasses().size() == 1) {
				return cls.getSuperClasses().get(0).getName();
			}
		}
		return "";		
	}

	/**
	 * Indica si la clase especificada es extendida por alguna otra clase.
	 * 
	 * @param elt
	 * @return
	 * @throws ENodeCastException
	 */
	public boolean isExtended(org.eclipse.uml2.uml.Element elt) throws ENodeCastException {
		org.eclipse.uml2.uml.Class cls = (org.eclipse.uml2.uml.Class) elt;

		// busco las asociaciones que son Generalization
		for (org.eclipse.uml2.uml.Relationship r : cls.getRelationships()) {
			if (r instanceof org.eclipse.uml2.uml.Generalization) {
				org.eclipse.uml2.uml.Generalization gen = (org.eclipse.uml2.uml.Generalization) r;
				if (gen.getGeneral().equals(cls)) {
					// si en la relación, la clase general coincide con la
					// pasada por parámetro
					if (gen.getGeneral().getGeneralizations().size() == 0) {
						// y esa clase, además no extiende a alguna otra
						return true;
					}
				}
			}
		}
		return false;
	}
	
	/**
	 * Retorna el nombre del element, o una cadena vacía si el elemento no es con nombre.
	 * 
	 * @param elt
	 * @return
	 */
	public String getName(org.eclipse.uml2.uml.Element elt) {
		if (elt instanceof org.eclipse.uml2.uml.NamedElement) {
			return ((org.eclipse.uml2.uml.NamedElement)elt).getName();
		}
		return "";
	}

	/**
	 * Traduce los tipos de datos a la plataforma específica. Particulamente, si
	 * el tipo es DataType, realiza una conversión.
	 * 
	 * @param type
	 * @return
	 */
	private String translateDataType(Type type) {
		String propTypeName = type.getName();
		if (type instanceof DataType) {
			// según el tipo lo traduzco a la plataforma AppEngine
			if (propTypeName.equalsIgnoreCase("Date")) { //$NON-NLS-1$
				propTypeName = "java.util.Date"; //$NON-NLS-1$
			} else if (propTypeName.equalsIgnoreCase("SystemUser")) { //$NON-NLS-1$
				propTypeName = "com.google.appengine.api.users.User"; //$NON-NLS-1$
			} else if (propTypeName.equalsIgnoreCase("Binary")) { //$NON-NLS-1$
				propTypeName = "byte[]"; //$NON-NLS-1$
			}
		}
		return propTypeName;
	}

	/**
	 * Genera los atributos de la clase. Si el atributo es primitivo copia el
	 * tipo, si es DataType, lo traduce a la plataforma específica.
	 * 
	 * @param cls
	 * @return
	 * @throws ENodeCastException
	 */
	public String generateAttributes(org.eclipse.uml2.uml.Class cls)
			throws ENodeCastException {
		StringBuilder sb = new StringBuilder();
		for (Property prop : cls.getAllAttributes()) {
			String propName = stringService.firstLower(prop.getName());
			String propTypeName = this.translateDataType(prop.getType());
			sb.append("\t@Basic").append('\n'); //$NON-NLS-1$
			sb.append("\tprivate ").append(propTypeName).append(" ") //$NON-NLS-1$ //$NON-NLS-2$
					.append(propName).append(";\n\n"); //$NON-NLS-1$
		}
		return sb.toString();
	}

	/**
	 * Genera los getteres y setters de la clase configurados para su
	 * persistencia con JPA. Este método utiliza el template:
	 * JPA.Accessor.template.txt
	 * 
	 * @param cls
	 *            un objeto de la meta-clase
	 * @return
	 * @throws ENodeCastException
	 */
	public String generateAccessors(org.eclipse.uml2.uml.Class cls)
			throws ENodeCastException {
		StringBuilder sb = new StringBuilder();
		for (Property prop : cls.getAttributes()) {
			String propName = stringService.firstLower(prop.getName());
			String methodName = stringService.firstUpper(prop.getName());
			String propTypeName = translateDataType(prop.getType());
			sb.append(TemplateReader.read(this, "JPA.Accessor.template.txt",
					propName, methodName, propTypeName));
		}
		return sb.toString();
	}

	/**
	 * Genera la clase Repository de una entidad.
	 * 
	 * @param cls
	 * @return
	 */
	public String generateRepository(org.eclipse.uml2.uml.Class cls) {
		String basePackage = "edu.idei.anteweb";
		String repoPackage = basePackage + ".repository";
		String entityPackage = basePackage + "." + cls.getPackage().getName();
		String entityName = cls.getName();
		String persistenceUnit = "transactions-optional";
		return TemplateReader.read(this, "JPA.Repository.template.txt", repoPackage,
				entityPackage, entityName, persistenceUnit);
	}

}
